using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using Framework.Helper;
using UnityEditor;
using UnityEngine;

public class PackagerBase : IPackager
{
    protected static PackagerRules packagerRules;

    private static string assetBundleDir;

    private static List<AssetBundleBuild> builds = new List<AssetBundleBuild>();
    private static Dictionary<string, List<string>> buildDetails = new Dictionary<string, List<string>>();

    public void LoadPackagerRules()
    {
        GameObject ruleObj = Resources.Load<GameObject>("Prefabs/PackagerRules");
        if (ruleObj == null)
            return;

        packagerRules = ruleObj.GetComponent<PackagerRules>();
    }

    public void PreparePackage()
    {
        string[] args = Environment.GetCommandLineArgs();
        foreach (string arg in args)
        {
            string[] childArray = arg.Split('=');
            string cmd = childArray[0];

            if (cmd.Equals("-dev"))
            {
                // TODO
            }
        }
        
        // 设置宏
        PlayerSettings.SetScriptingDefineSymbolsForGroup(Packager.BuildGroup, PackagerSettings.MACROS);
    }
    
    // 准备资源目录
    public string PreparePackageDir()
    {
        string packagePath = $"{Application.dataPath}/../{Packager.PUBLISHING_DIR}";
        
        if (Directory.Exists(packagePath))
            Directory.Delete(packagePath, true);

        Directory.CreateDirectory(packagePath);

        return packagePath;
    }
    
    // 获取应用扩展名
    public string GetAppExtension()
    {
        switch (Packager.BuildTarget)
        {
            case BuildTarget.Android:
                return ".apk";
            case BuildTarget.StandaloneWindows:
            case BuildTarget.StandaloneWindows64:
                return ".exe";
        }

        return "";
    }
    
    // 搜集要打包的资源信息
    public void CollectBuildAssets()
    {
        if (packagerRules.packingPaths.Length <= 0)
            return;

        List<string> paths = new List<string>();
        List<string> files = new List<string>();

        for (int i = 0; i < packagerRules.packingPaths.Length; i++)
        {
            string packingPath = packagerRules.packingPaths[i];
            packingPath = packingPath.Replace("\\", "/");
            packingPath = packingPath.Trim();
            
            Recursive(paths, files, packingPath, (path, isDir) =>
            {
                // 忽略路径
                for (int j = 0; j < packagerRules.ignorePaths.Length; j++)
                {
                    // Debug.Log("====  "+ path + " " + packagerRules.ignorePaths[j]);
                    if (path.StartsWith(packagerRules.ignorePaths[j], StringComparison.OrdinalIgnoreCase))
                        return false;
                }

                if (!isDir)
                {
                    // 忽略扩展名
                    string ext = Path.GetExtension(path);
                    for (int j = 0; j < packagerRules.ignoreExterns.Length; j++)
                    {
                        if (ext.Equals(packagerRules.ignoreExterns[j], StringComparison.OrdinalIgnoreCase))
                            return false;
                    }
                    
                    // 文件名忽略
                    string fileName = Path.GetFileName(path);
                    for (int j = 0; j < packagerRules.ignoreFiles.Length; j++)
                    {
                        if (fileName.Equals(packagerRules.ignoreFiles[j], StringComparison.OrdinalIgnoreCase))
                            return false;
                    }
                }
                
                return true;
            });
        }
        
        builds.Clear();
        buildDetails.Clear();
        Dictionary<string, string> bundleRecord = new Dictionary<string, string>();

        for (int i = 0; i < files.Count; i++)
        {
            string bundleName = packagerRules.File2AssetBundleName(files[i]);
            bool needCompress = packagerRules.NeedCompress(files[i]);

            string bundlePath = needCompress ? bundleName : bundleName.Substring(0, bundleName.LastIndexOf('.'));
            bundlePath = $"{assetBundleDir.ToLower()}/{bundlePath}";

            if (needCompress)
            {
                // Debug.Log("AAA  " + bundleName + " " + bundlePath);
                if (!buildDetails.TryGetValue(bundlePath, out List<string> fileList))
                {
                    fileList = new List<string>();
                    fileList.Add(files[i]);

                    buildDetails.Add(bundlePath, fileList);
                    
                    bundleRecord.Add(bundlePath, bundleName);
                }
                else
                {
                    fileList.Add(files[i]);
                }
            }
        }

        foreach (KeyValuePair<string, List<string>> detail in buildDetails)
        {
            // Debug.Log("####  " + detail.Key + " " + bundleRecord[detail.Key] + " " + detail.Value.Count);
            AssetBundleBuild build = new AssetBundleBuild();
            build.assetBundleName = bundleRecord[detail.Key];
            build.assetNames = detail.Value.ToArray();
            builds.Add(build);
        }
    }

    public void BuildAssetBundle()
    {
        if (!Directory.Exists(Packager.ASSET_BUNDLE_DIR))
            Directory.CreateDirectory(Packager.ASSET_BUNDLE_DIR);

        assetBundleDir = $"{Packager.ASSET_BUNDLE_DIR}/{Packager.BuildTarget.ToString()}";
        if (!Directory.Exists(assetBundleDir))
            Directory.CreateDirectory(assetBundleDir);
        
        LoadPackagerRules();
        PreparePackage();
        CollectBuildAssets();
        
        // copy无需压缩文件
        CopyNoCompressFiles();

        BuildPipeline.BuildAssetBundles(
            assetBundleDir,
            builds.ToArray(),
            BuildAssetBundleOptions.DeterministicAssetBundle | BuildAssetBundleOptions.ChunkBasedCompression,
            Packager.BuildTarget);

        string manifestPath = $"{Application.dataPath}/../{assetBundleDir}/{Packager.BuildTarget.ToString()}";
        AssetBundle assetBundle = AssetBundle.LoadFromMemory(File.ReadAllBytes(manifestPath));
        AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

        AssetBundleGraph graph = new AssetBundleGraph(manifest);
        if (graph.EliminateCycle())
        {
            Debug.LogError("[Packager] There is cycle dependencies in AssetBundles. " + graph.ToString("AssetBundle Dependencies:"));
        }

        CreateChecksum(manifest);
        
        // assetBundle.Unload(false);
        
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
    }
    
    // 生成player或者相应的工程
    public virtual void BuildPlayer(bool export = true) { }

    public void CopyAssetBundles2StreamingAssets()
    {
        if (Directory.Exists(Application.streamingAssetsPath))
        {
            Directory.Delete(Application.streamingAssetsPath, true);
        }
        Directory.CreateDirectory(Application.streamingAssetsPath);

        string dstDir = Application.dataPath + "/StreamingAssets";
        CopyBundle2TargetPath(dstDir);
    }

    public void CopyBundle2TargetPath(string targetDirectory)
    {
        string buildTarget = Packager.BuildTarget.ToString();

        FileHelper.CopyFiles2TargetDirRecursive(Application.dataPath.Substring(0, Application.dataPath.LastIndexOf('/')) + "/AssetBundles/" + buildTarget, targetDirectory, (string filename) =>
        {
            return Path.GetExtension(filename) != ".manifest";
        }, null, (srcPath, dstPath) =>
        {
            string fileExtension = Path.GetExtension(srcPath);
            if (packagerRules != null && !string.IsNullOrEmpty(fileExtension) && !fileExtension.Equals(packagerRules.bundleExtension))
            {
                Debug.Log($"[Packager] copy file path: {srcPath}");
                return true;
            }
            
            Debug.Log($"[Packager] encode file path: {srcPath}");
            bool needEncode = EncodeAssetBundle(srcPath, dstPath);
            if (!needEncode)
                return true;
            
            return false;
        });
        
        Debug.Log("copy success.");
        AssetDatabase.Refresh();
    }
    
    // 加密
    private bool EncodeAssetBundle(string src, string dst)
    {
        if (AssetBundleHelper.abOffset <= 0)
            return false;
        
        Debug.Log($"=== [Packager] EncodeAssetBundle  srcPath: { src }");
        if (!File.Exists(src))
        {
            Debug.LogError($"=== [Packager] EncodeAssetBundle  can not find file: { src } !!!!");
            return false;
        }
        
        uint abOffset = AssetBundleHelper.abOffset;

        byte[] fileData = File.ReadAllBytes(src);
        int fileLen = (int)abOffset + fileData.Length;
        byte[] buffer = new byte[fileLen];

        for(int i = 0; i < abOffset; i++)
        {
            buffer[i] = 1;
        }

        for(int i = 0; i < fileData.Length; i++)
        {
            buffer[i + abOffset] = fileData[i]; 
        }
        Debug.Log($"=== [Packager] EncodeAssetBundle  src size: { fileLen }");

        FileStream fs = File.OpenWrite(dst);
        fs.Write(buffer, 0, fileLen);
        fs.Close();

        return true;
    }
    
    // 生成checksum文件
    private void CreateChecksum(AssetBundleManifest manifest)
    {
        string checksumAsset = $"Assets/{Packager.CHECKSUM_FILE}";
        
        ResourceChecksumData data = LoadChecksumData(checksumAsset);
        data.Clear();

        string checksumBundleName = checksumAsset + packagerRules.bundleExtension.ToLower();

        string checksumBundleOutputDir = assetBundleDir + "_Checksum";
        
        int clientVersion = 0;
        try
        {
            string version = File.ReadAllText(Application.streamingAssetsPath + "/" + PackagerSettings.versionFile);
            int.TryParse(version, out clientVersion);
        }
        catch (Exception e)
        {
            Debug.Log("load streamingassets version.txt error, error: " + e.Message);
            clientVersion = 0;
        }
        
        if (clientVersion > 0)
            checksumBundleOutputDir += $"_{clientVersion}";
        
        if (Directory.Exists(checksumBundleOutputDir))
            Directory.Delete(checksumBundleOutputDir, true);
        if (!Directory.Exists(checksumBundleOutputDir))
            Directory.CreateDirectory(checksumBundleOutputDir);

        ulong totalSize = 0;
        int total = 0;
        
        // 根据manifest创建checksum文件
        // 可用于分析哪些AssetBundle过大，减小包体
        Dictionary<string, int> bigFiles = new Dictionary<string, int>();

        string checksumTextAsset = $"{checksumBundleOutputDir}/checksum.txt";
        using (StreamWriter sw = new StreamWriter(checksumTextAsset))
        {
            foreach (string bundleName in manifest.GetAllAssetBundles())
            {
                Debug.Log("====  " + bundleName);
                if (string.IsNullOrEmpty(bundleName) || string.IsNullOrEmpty(bundleName.Trim()))
                    continue;

                string filePath = $"{assetBundleDir}/{bundleName}";

                FileInfo fi = new FileInfo(filePath);
                int fileSize = (int) fi.Length;

                string hash = "";
                string fileExt = Path.GetExtension(filePath);
                if (fileExt.Equals($".unity{packagerRules.bundleExtension}"))
                    hash = manifest.GetAssetBundleHash(bundleName).ToString();
                else
                    hash = CalculateAssetBundleMD5(filePath.ToLower());
                
                data.Add(bundleName, fileSize, hash);
                
                sw.WriteLine($"{bundleName}, {fileSize / 1024}K, {hash}");

                totalSize += (ulong) fileSize;
                ++total;

                if (fileSize > 1024 * 1024)
                {
                    bigFiles.Add(filePath, fileSize);
                }
            }

            AppendAdditionChecksum(packagerRules.dir2NoCompress, sw, data);
        }
        
        EditorUtility.SetDirty(data);
        AssetDatabase.SaveAssets();
        
        AssetDatabase.ImportAsset(checksumAsset);
        AssetDatabase.Refresh();

        List<AssetBundleBuild> checksumBuilds = new List<AssetBundleBuild>();
        
        AssetBundleBuild build = new AssetBundleBuild();
        build.assetNames = new[] {checksumAsset};
        build.assetBundleName = checksumBundleName;
        checksumBuilds.Add(build);
        
        // 创建checksum AssetBundle
        BuildPipeline.BuildAssetBundles(
            checksumBundleOutputDir,
            checksumBuilds.ToArray(),
            BuildAssetBundleOptions.ChunkBasedCompression,
            Packager.BuildTarget
        );
        
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
        
        foreach (string p in bigFiles.Keys)
        {
            Debug.Log(string.Format("[Packager] Big Bundle file: {0} : {1}K", p, bigFiles[p] / 1024));
        }
        
        Debug.Log(string.Format("[Packager] Finish generate checksum file for {0} files with size : {1}", total, totalSize));
        
        string targetPath = $"{assetBundleDir}/{checksumBundleName}";
        if (File.Exists(targetPath))
        {
            File.Delete(targetPath);
        }
        
        // 保存到指定指定位置
        File.Copy($"{checksumBundleOutputDir}/{checksumBundleName}", targetPath);
        
        // 保存版本号
        File.WriteAllText($"{assetBundleDir}/Version.txt", clientVersion.ToString());
        
        Debug.Log("[Packager] Write checksum file success");
        
        targetPath = $"{checksumBundleOutputDir}/checksum";
        if (File.Exists(checksumAsset))
        {
            File.Move(checksumAsset, targetPath);
            Debug.Log("[Packager] Move checksum file to " + targetPath);
        }
        if (File.Exists(checksumAsset + ".meta"))
        {
            File.Delete(checksumAsset + ".meta");
        }
        
        AssetDatabase.Refresh();
    }
    
    // 追加一些资源到checksum文件内
    private static void AppendAdditionChecksum(string[] directories, StreamWriter sw, ResourceChecksumData data)
    {
        for (int i = 0; i < directories.Length; i++)
        {
            string dir = directories[i];
            string fileDir = $"{assetBundleDir}/{dir}";
            if (!Directory.Exists(fileDir))
                continue;

            string[] files = Directory.GetFiles(fileDir);
            AddFiles2Checksum(files, sw, data);
        }
    }
    
    private static void AddFiles2Checksum(string[] files, StreamWriter sw, ResourceChecksumData data, bool needSub = true)
    {
        for (int j = 0; j < files.Length; j++)
        {
            string filename = files[j].Replace('\\', '/');
            byte[] filemd5 = FileHelper.CreateFileMD5(filename);
            string md5 = BitConverter.ToString(filemd5).Replace("-", "").ToLower();

            FileInfo fi = new FileInfo(filename);
            int fileSize = (int)fi.Length;

            string relatedName = filename.Substring(assetBundleDir.Length + 1);
            if (needSub)
                relatedName = relatedName.ToLower();

            sw.WriteLine($"{relatedName}, {fileSize / 1024}K, {md5}");
            
            data.Add(relatedName, fileSize, md5);
        }  
    }
    
    // 载入checksum文件
    private static ResourceChecksumData LoadChecksumData(string checksumAssetPath)
    {
        ResourceChecksumData data = AssetDatabase.LoadAssetAtPath<ResourceChecksumData>(checksumAssetPath);
        if (data == null)
        {
            data = ScriptableObject.CreateInstance<ResourceChecksumData>();
            AssetDatabase.CreateAsset(data, checksumAssetPath);
            AssetDatabase.SaveAssets();
            data = AssetDatabase.LoadAssetAtPath<ResourceChecksumData>(checksumAssetPath);
        }

        return data;
    }
    
    // 把无需压缩的文件copy到相应文件夹
    private static void CopyNoCompressFiles()
    {
        for (int i = 0; i < packagerRules.dir2NoCompress.Length; i++)
        {
            string path = packagerRules.dir2NoCompress[i];

            string externalPath = $"{Application.dataPath}/../{Packager.ASSET_BUNDLE_DIR}/{Packager.BuildTarget.ToString()}/{path.ToLower()}";
            if (!Directory.Exists(externalPath))
                Directory.CreateDirectory(externalPath);

            string src = $"{Application.dataPath}/../{path}";
            FileHelper.CopyFiles2TargetDirRecursive(
                src, 
                externalPath, 
                (fileName) => packagerRules.FileWithNoCompressExtern(fileName), 
                (fileName) => fileName.ToLower()
            );
        }
    }
    
    // 递归遍历文件夹获取符合条件的路径信息
    protected static void Recursive(List<string> allPaths, List<string> allFiles, string targetDir,
        Func<string, bool, bool> filter, bool needSub = true)
    {
        string[] files = Directory.GetFiles(targetDir);
        
        // 遍历文件
        foreach (string file in files)
        {
            string normalize = file.Replace('\\', '/');
            if (needSub)
                normalize = normalize.Substring(normalize.IndexOf("Assets/", StringComparison.Ordinal));

            if (filter != null && !filter(normalize, false))
                continue;
            
            allFiles.Add(normalize);
        }

        string[] dirs = Directory.GetDirectories(targetDir);
        // 遍历文件夹
        foreach (string dir in dirs)
        {
            string relateDir = dir.Replace('\\', '/');
            if (needSub)
                relateDir = relateDir.Substring(relateDir.IndexOf("Assets/", StringComparison.Ordinal));
            
            if (filter != null && !filter(relateDir, true))
                continue;
            
            allPaths.Add(relateDir);
            
            // 递归
            Recursive(allPaths, allFiles, dir, filter, needSub);
        }
    }
    
    // 计算AssetBundle的hash值，由子文件MD5构成
    private static string CalculateAssetBundleMD5(string bundleName)
    {
        // Debug.Log("DDDD  " + bundleName + " " + buildDetails.ContainsKey(bundleName));
        if (!buildDetails.ContainsKey(bundleName) || buildDetails[bundleName] == null || buildDetails[bundleName].Count <= 0)
        {
            Debug.Log(buildDetails.ContainsKey(bundleName));
            Debug.Log(buildDetails[bundleName]);
            Debug.Log(buildDetails[bundleName].Count);
			
            KeyNotFoundException noKeyException = new KeyNotFoundException(string.Format("{0} bundle needs checksum hash value, but build details has error.", bundleName));
            throw noKeyException;
        }
        
        List<string> filenames = buildDetails[bundleName];
        // Debug.Log("VVV  " + bundleName + " " + filenames.Count);
        filenames.Sort();
        List<byte> hashBytes = new List<byte>();

        foreach (string filename in filenames)
        {
            byte[] fileMD5Content = FileHelper.CalculateFileMD5(filename);
            foreach (byte b in fileMD5Content)
            {
                hashBytes.Add(b);
            }
        }

        MD5 md5 = MD5.Create();
        byte[] hash = md5.ComputeHash(hashBytes.ToArray());
        return BitConverter.ToString(hash).Replace("-", "").ToLower();
    }
}
